﻿using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace OxyPlot.CatExtension
{
    public sealed class PointsDecimator
    {
        public const int MIN_DECIMATE_COUNT = 512;
        public const int MIN_DECIMATE_PARALLEL_COUNT = 1024 * 8;

        public int ProcessorCount { get; set; }
        private readonly List<ScreenPoint>[] _buffers;

        private double offset_x;
        private double scale_x;

        private double offset_y;
        private double scale_y;

        public PointsDecimator()
        {
            ProcessorCount = Environment.ProcessorCount;
            _buffers = new List<ScreenPoint>[ProcessorCount];
            for (int i = 0; i < ProcessorCount; i++)
            {
                _buffers[i] = [];
            }
        }

        public void Decimate(List<DataPoint> input, int start, int end, double offsetX, double scaleX, double offsetY, double scaleY, List<ScreenPoint> output)
        {
            if (input == null || input.Count == 0)
            {
                return;
            }

            offset_x = offsetX;
            scale_x = scaleX;
            offset_y = offsetY;
            scale_y = scaleY;

            var count = end - start + 1;

            if (count <= MIN_DECIMATE_COUNT)
            {
                for (int i = start; i <= end; i++)
                {
                    output.Add(Transform(input[i]));
                }
            }
            else if (count < MIN_DECIMATE_PARALLEL_COUNT)
            {
                DecimateCore(input, start, end, output);
            }
            else
            {
                DecimateParallel(input, start, end, output);
            }

        }

        private void DecimateCore(List<DataPoint> input, int start, int end, List<ScreenPoint> output)
        {
            var screenPoints = CollectionsMarshal.AsSpan(input).Slice(start, end - start + 1);

            var point = Transform(screenPoints[0]);
            int currentX = (int)point.X;
            double firstY = point.Y;
            double minY = firstY, maxY = firstY;

            for (int i = 1; i < screenPoints.Length; i++)
            {
                point = Transform(screenPoints[i]);
                var newX = point.X;
                var newY = point.Y;

                if (currentX != (int)newX)
                {
                    output.Add(new ScreenPoint(currentX, firstY));

                    if (minY != firstY)
                    {
                        output.Add(new ScreenPoint(currentX, minY));
                    }

                    if (maxY != minY)
                    {
                        output.Add(new ScreenPoint(currentX, maxY));
                    }

                    currentX = (int)newX;
                    firstY = minY = maxY = newY;
                }
                else
                {
                    minY = Math.Min(minY, newY);
                    maxY = Math.Max(maxY, newY);
                }
            }

            output.Add(Transform(screenPoints[^1]));
        }

        private void DecimateParallel(List<DataPoint> input, int start, int end, List<ScreenPoint> output)
        {
            var len = (end - start + 1) / ProcessorCount;
            var lastLen = (end - start + 1) % ProcessorCount + len;
            Parallel.For(0, ProcessorCount, i =>
            {
                var buffer = _buffers[i];

                var span = CollectionsMarshal.AsSpan(input);
                Span<DataPoint> screenPoints;
                if (i == ProcessorCount - 1)
                {
                    screenPoints = span.Slice(start + i * len, lastLen);
                }
                else
                {
                    screenPoints = span.Slice(start + i * len, len);
                }

                var point = Transform(screenPoints[0]);
                int currentX = (int)point.X;
                double firstY = point.Y;
                double minY = firstY, maxY = firstY;

                for (int j = 1; j < screenPoints.Length; j++)
                {
                    point = Transform(screenPoints[j]);
                    var newX = point.X;
                    var newY = point.Y;

                    if (currentX != (int)newX)
                    {
                        buffer.Add(new ScreenPoint(currentX, firstY));

                        if (minY != firstY)
                        {
                            buffer.Add(new ScreenPoint(currentX, minY));
                        }

                        if (maxY != minY)
                        {
                            buffer.Add(new ScreenPoint(currentX, maxY));
                        }

                        currentX = (int)newX;
                        firstY = minY = maxY = newY;
                    }
                    else
                    {
                        minY = Math.Min(minY, newY);
                        maxY = Math.Max(maxY, newY);
                    }
                }
                buffer.Add(Transform(screenPoints[^1]));
            });

            for (int i = 0; i < _buffers.Length; i++)
            {
                output.AddRange(_buffers[i]);
                _buffers[i].Clear();
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private ScreenPoint Transform(DataPoint point)
        {
            return new ScreenPoint((point.X - offset_x) * scale_x, (point.Y - offset_y) * scale_y);
        }
    }
}
